use argon2::{
    password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
    Argon2, PasswordHash, PasswordVerifier,
};
use chrono::Duration;
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};

use crate::error::ErrorKind;

const MAX_PASSWORD_LENGTH: usize = 64;

pub fn hash_password(password: impl AsRef<str>) -> Result<String, ErrorKind> {
    let password = password.as_ref();

    if password.is_empty() {
        return Err(ErrorKind::EmptyPassword);
    }

    if password.len() > MAX_PASSWORD_LENGTH {
        return Err(ErrorKind::ExceedMaxPasswordLength(MAX_PASSWORD_LENGTH));
    }

    let salt = SaltString::generate(&mut OsRng);
    let hashed_password = Argon2::default()
        .hash_password(password.as_bytes(), &salt)
        .map_err(|_| ErrorKind::HashError)?
        .to_string();

    Ok(hashed_password)
}

pub fn compare_passwords(password: &str, hashed_password: &str) -> Result<bool, ErrorKind> {
    if password.is_empty() {
        return Err(ErrorKind::EmptyPassword);
    }

    if password.len() > MAX_PASSWORD_LENGTH {
        return Err(ErrorKind::ExceedMaxPasswordLength(MAX_PASSWORD_LENGTH));
    }

    let parsed_hash =
        PasswordHash::new(hashed_password).map_err(|_| ErrorKind::InvalidHashFormat)?;

    let matched = Argon2::default()
        .verify_password(password.as_bytes(), &parsed_hash)
        .map_or(false, |_| true);

    Ok(matched)
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenClaims {
    pub sub: String,
    pub iat: usize,
    pub exp: usize,
}

pub fn encode_token(
    user_id: &str,
    secret: &[u8],
    expires_in_sec: i64,
) -> jsonwebtoken::errors::Result<String> {
    if user_id.is_empty() {
        return Err(jsonwebtoken::errors::ErrorKind::InvalidSubject.into());
    }

    let now = chrono::Local::now();
    let iat = now.timestamp() as usize;
    let exp = (now + Duration::seconds(expires_in_sec)).timestamp() as usize;

    let claims = TokenClaims {
        sub: user_id.to_owned(),
        iat,
        exp,
    };

    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(secret),
    )
}

pub fn decode_token(token: &str, secret: &[u8]) -> jsonwebtoken::errors::Result<String> {
    let key = DecodingKey::from_secret(secret);

    let validation = Validation::new(jsonwebtoken::Algorithm::HS256);

    let decode = decode::<TokenClaims>(token, &key, &validation)?;

    if decode.claims.exp < chrono::Local::now().timestamp() as usize {
        return Err(jsonwebtoken::errors::ErrorKind::ExpiredSignature.into());
    }

    Ok(decode.claims.sub)
}
